package com.tn.installer;

import static org.apache.commons.lang.StringUtils.isNotEmpty;

import java.io.File;
import java.io.OutputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.xml.stream.XMLStreamWriter;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.xpath.XPathFactory;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.w3c.dom.Element;
import org.w3c.dom.Node;

import com.tn.installer.xml.PrettyPrintXMLStreamWriter;

/**
 * Writes the install script.
 */
public class InstallScriptWriter
{
  private static final String ATTRIBUTE_DEST = "dest";
  private static final String ATTRIBUTE_DIRECTORY = "dir";
  private static final String ATTRIBUTE_LINK = "link";
  private static final String ATTRIBUTE_NAME = "name";
  private static final String ATTRIBUTE_PASSWORD = "password";
  private static final String ATTRIBUTE_RESOURCE = "resource";
  private static final String ATTRIBUTE_SOURCE = "src";
  private static final String ATTRIBUTE_URL = "url";
  private static final String ATTRIBUTE_USERNAME = "username";
  private static final String ELEMENT_CREATE_SYMLINK = "createsymlink";
  private static final String ELEMENT_DELETE = "delete";
  private static final String ELEMENT_DELETE_SYMLINK = "deletesymlink";
  private static final String ELEMENT_IMPORT = "import";
  private static final String ELEMENT_INSTALL_ASSEMBLY = "installassembly";
  private static final String ELEMENT_JAVA_RESOURCE = "javaresource";
  private static final String ELEMENT_MAKE_DIRECTORY = "mkdir";
  private static final String ELEMENT_PROJECT = "project";
  private static final String ELEMENT_TARGET = "target";
  private static final String ELEMENT_TASKDEF = "taskdef";
  private static final String VALUE_ANT_CONTRIB_PROPERTIES = "net/sf/antcontrib/antcontrib.properties";
  private static final String VALUE_INSTALL = "install";
  private static final String VALUE_INSTALL_DIRECTORY = "./.install";
  private static final String VALUE_INSTALL_MACROS = "conf/install-macros.xml";
  private static final String VALUE_UNINSTALL = "uninstall";
  private static final String ELEMENT_UNINSTALL_ASSEMBLIES = "uninstallassemblies";
  private static final XPath  XPATH = XPathFactory.newInstance().newXPath();
  private static final String XPATH_AFTER_INSTALL = "/deployment/afterInstall/*";
  private static final String XPATH_AFTER_UNINSTALL = "/deployment/afterUninstall/*";
  private static final String XPATH_ASSEMBLY = "/deployment/assembly";
  private static final String XPATH_BEFORE_INSTALL = "/deployment/beforeInstall/*";
  private static final String XPATH_BEFORE_UNINSTALL = "/deployment/beforeUninstall/*";
  private static final String XPATH_DEPLOYMENT_NAME = "/deployment/@name";
  private static final String XPATH_DIRECTORY = "/deployment/directory";
  private static final String XPATH_MACRODEF = "/deployment/macrodef";
  private static final String XPATH_SYMLINK = "/deployment/symlink";

  private static final String DEFAULT_ENCODING = "UTF-8";
  private static final String DEFAULT_VERSION = "1.0";
  private static final String DO_NOT_MODIFY_WARNING = "This script was generated by the Trust Nick installer and modifying it may cause " +
    "future uninstalls to fail.";

  private Document deploymentXml;

  /**
   * Creates a new <code>InstallScriptWriter</code>.
   *
   * @param deploymentXml the deployment descriptor.
   */
  public InstallScriptWriter(Document deploymentXml)
  {
    this.deploymentXml = deploymentXml;
  }

  /**
   *
   */
  @SuppressWarnings({"ConstantConditions"})
  public void writeInstallScript(File installScriptXml) throws XMLStreamException, IOException, XPathExpressionException
  {
    OutputStream out = null;
    XMLStreamWriter writer = null;

    try
    {
      out = new FileOutputStream(installScriptXml);
      writer = new PrettyPrintXMLStreamWriter(XMLOutputFactory.newInstance().createXMLStreamWriter(out, DEFAULT_ENCODING));

      writer.writeStartDocument(DEFAULT_ENCODING, DEFAULT_VERSION);
      {
        writer.writeStartElement(ELEMENT_PROJECT);
        {
          writer.writeAttribute(ATTRIBUTE_NAME, XPATH.evaluate(XPATH_DEPLOYMENT_NAME, deploymentXml, XPathConstants.STRING) + "-install");

          writer.writeComment(DO_NOT_MODIFY_WARNING);

          writer.writeEmptyElement(ELEMENT_TASKDEF);
          writer.writeAttribute(ATTRIBUTE_RESOURCE, VALUE_ANT_CONTRIB_PROPERTIES);

          writer.writeStartElement(ELEMENT_IMPORT);
          writer.writeEmptyElement(ELEMENT_JAVA_RESOURCE);
          writer.writeAttribute(ATTRIBUTE_NAME, VALUE_INSTALL_MACROS);
          writer.writeEndElement();

          writeInstallTarget(deploymentXml, writer);
          writeUninstallTarget(deploymentXml, writer);          

          writeMacroDefs((NodeList)XPATH.evaluate(XPATH_MACRODEF, deploymentXml, XPathConstants.NODESET), writer);
        }
        writer.writeEndElement();
      }
      writer.writeEndDocument();
    }
    finally
    {
      if (writer != null)
      {
        writer.flush();
        writer.close();
      }

      if (out != null)
      {
        out.close();
      }
    }
  }

  /**
   * Copies the <code>element</code>'s attributes into the writer.
   */
  private void copyAttributes(Element element, XMLStreamWriter writer) throws XMLStreamException
  {
    for (int i = 0; i < element.getAttributes().getLength(); i++)
    {
      writer.writeAttribute(element.getAttributes().item(i).getNodeName(), element.getAttributes().item(i).getNodeValue());
    }
  }

  /**
   * Copies the <code>childElements</code> into the writer.
   */
  private void copyChildElements(NodeList childElements, XMLStreamWriter writer) throws XMLStreamException
  {
    for (int i = 0; i < childElements.getLength(); i++)
    {
      if (childElements.item(i) instanceof Element)
      {
        Element childElement = (Element)childElements.item(i);

        if (isNotEmpty(childElement.getTagName()))
        {
          if (childElement.hasChildNodes())
          {
            writer.writeStartElement(childElement.getTagName());
            {
              copyAttributes(childElement, writer);
              copyChildElements(childElement.getChildNodes(), writer);
            }
            writer.writeEndElement();
          }
          else
          {
            writer.writeEmptyElement(childElement.getTagName());
            copyAttributes(childElement, writer);
          }
        }
      }
    }
  }

  /**
   * Writes the assemblies to the install target.
   */
  private void writeInstallAssemblies(NodeList assemblies, XMLStreamWriter writer) throws XMLStreamException
  {
    for (int i = 0; i < assemblies.getLength(); i++)
    {
      Element assembly = (Element)assemblies.item(i);

      writer.writeEmptyElement(ELEMENT_INSTALL_ASSEMBLY);
      writer.writeAttribute(ATTRIBUTE_SOURCE, assembly.getAttribute(ATTRIBUTE_URL));
      writer.writeAttribute(ATTRIBUTE_DEST, VALUE_INSTALL_DIRECTORY);

      String username = assembly.getAttribute(ATTRIBUTE_USERNAME);
      String password = assembly.getAttribute(ATTRIBUTE_PASSWORD);

      if (isNotEmpty(username) && isNotEmpty(password))
      {
        writer.writeAttribute(ATTRIBUTE_USERNAME, username);
        writer.writeAttribute(ATTRIBUTE_PASSWORD, password);
      }
    }
  }

  /**
   * Writes the directories to the install target.
   */
  private void writeInstallDirectories(NodeList directories, XMLStreamWriter writer) throws XMLStreamException
  {
    for (int i = 0; i < directories.getLength(); i++)
    {
      Element directory = (Element)directories.item(i);

      writer.writeEmptyElement(ELEMENT_MAKE_DIRECTORY);
      writer.writeAttribute(ATTRIBUTE_DIRECTORY, directory.getAttribute(ATTRIBUTE_NAME));
    }
  }

  /**
   * Writes the symlinks to the install target.
   */
  private void writeInstallSymLinks(NodeList symlinks, XMLStreamWriter writer) throws XMLStreamException
  {
    for (int i = 0; i < symlinks.getLength(); i++)
    {
      Element symlink = (Element)symlinks.item(i);

      writer.writeEmptyElement(ELEMENT_CREATE_SYMLINK);
      writer.writeAttribute(ATTRIBUTE_LINK, symlink.getAttribute(ATTRIBUTE_LINK));
      writer.writeAttribute(ATTRIBUTE_RESOURCE, symlink.getAttribute(ATTRIBUTE_RESOURCE));
    }
  }

  /**
   * Writes the install target.
   */
  private void writeInstallTarget(Document deploymentXml, XMLStreamWriter writer) throws XMLStreamException, XPathExpressionException
  {
    writer.writeStartElement(ELEMENT_TARGET);
    {
      writer.writeAttribute(ATTRIBUTE_NAME, VALUE_INSTALL);

      copyChildElements((NodeList)XPATH.evaluate(XPATH_BEFORE_INSTALL, deploymentXml, XPathConstants.NODESET), writer);

      writeInstallAssemblies((NodeList)XPATH.evaluate(XPATH_ASSEMBLY, deploymentXml, XPathConstants.NODESET), writer);
      writeInstallDirectories((NodeList)XPATH.evaluate(XPATH_DIRECTORY, deploymentXml, XPathConstants.NODESET), writer);
      writeInstallSymLinks((NodeList)XPATH.evaluate(XPATH_SYMLINK, deploymentXml, XPathConstants.NODESET), writer);

      copyChildElements((NodeList)XPATH.evaluate(XPATH_AFTER_INSTALL, deploymentXml, XPathConstants.NODESET), writer);
    }
    writer.writeEndElement();
  }

  /**
   * Writes the macro def elements.
   */
  private void writeMacroDefs(NodeList macroDefElements, XMLStreamWriter writer) throws XMLStreamException
  {
    for (int i = 0; i < macroDefElements.getLength(); i++)
    {
      Element macroDefElement = (Element)macroDefElements.item(i);

      writer.writeStartElement(macroDefElement.getTagName());
      copyAttributes(macroDefElement,  writer);
      copyChildElements(new SingleItemNodeList(macroDefElement), writer);
      writer.writeEndElement();
    }
  }

  /**
   * Writes the directories to the uninstall target.
   */
  private void writeUninstallAssemblies(XMLStreamWriter writer) throws XMLStreamException, XPathExpressionException {

    writer.writeEmptyElement(ELEMENT_UNINSTALL_ASSEMBLIES);
  }

  /**
   * Writes the directories to the uninstall target.
   */
  private void writeUninstallDirectories(NodeList directories, XMLStreamWriter writer) throws XMLStreamException, XPathExpressionException
  {
    for (int i = 0; i < directories.getLength(); i++)
    {
      Element directory = (Element)directories.item(i);

      writer.writeEmptyElement(ELEMENT_DELETE);
      writer.writeAttribute(ATTRIBUTE_DIRECTORY, directory.getAttribute(ATTRIBUTE_NAME));
    }      
  }

  /**
   * Writes the symlinks to the uninstall target.
   */
  private void writeUninstallSymLinks(NodeList symlinks, XMLStreamWriter writer) throws XMLStreamException, XPathExpressionException
  {
    for (int i = 0; i < symlinks.getLength(); i++)
    {
      Element symlink = (Element)symlinks.item(i);

      writer.writeEmptyElement(ELEMENT_DELETE_SYMLINK);
      writer.writeAttribute(ATTRIBUTE_LINK, symlink.getAttribute(ATTRIBUTE_LINK));
      writer.writeAttribute(ATTRIBUTE_RESOURCE, symlink.getAttribute(ATTRIBUTE_RESOURCE));
    }
  }

  /**
   * Writes the uninstall target.
   */
  private void writeUninstallTarget(Document deploymentXml, XMLStreamWriter writer) throws XMLStreamException, XPathExpressionException
  {
    writer.writeStartElement(ELEMENT_TARGET);
    {
      writer.writeAttribute(ATTRIBUTE_NAME, VALUE_UNINSTALL);

      copyChildElements((NodeList)XPATH.evaluate(XPATH_BEFORE_UNINSTALL, deploymentXml, XPathConstants.NODESET), writer);

      writeUninstallSymLinks((NodeList)XPATH.evaluate(XPATH_SYMLINK, deploymentXml, XPathConstants.NODESET), writer);
      writeUninstallDirectories((NodeList)XPATH.evaluate(XPATH_DIRECTORY, deploymentXml, XPathConstants.NODESET), writer);
      writeUninstallAssemblies(writer);

      copyChildElements((NodeList)XPATH.evaluate(XPATH_AFTER_UNINSTALL, deploymentXml, XPathConstants.NODESET), writer);
    }
    writer.writeEndElement();
  }

  /**
   * An implementation of <code>NodeList</code> that wraps a single <code>Node</code>.
   */
  private class SingleItemNodeList implements NodeList
  {
    private Node item;

    /**
     * Creates a new <code>SingleItemNodeList</code> initialized with the <code>item</code>.
     */
    public SingleItemNodeList(Node item)
    {
      this.item = item;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node item(int index)
    {
      if (index != 0)
      {
        throw new IndexOutOfBoundsException();
      }

      return item;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getLength()
    {
      return 1;
    }
  }
}
